/**
 *
 */
package de.saar.penguin.give2.server;

import give.formula.*;
import give.nlgserver.NlgServerException;
import give.nlgserver.discretize.*;
import give.world.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import give.util.GeometryTools;
import java.lang.*;


/** This class watches the player on their way to the target.
 * It keeps the current plan and updates every time the method <code>handleStatusInformation</code>
 * or <code>handleAction</code> gets called.
 * The plan is held in <code>currentPlan</code>. Usually the head of the plan gets removed
 * as the player "walks" through the plan. In some cases it is necessary to recompute the plan,
 * e.g. if the user has moved to a region not contained in the remaining atoms of the plan.
 * (Atoms = smallest units of the plan, consisting of a predicate like <code>move</code>
 * and parameters (like target region)).
 * The history of the player is stored in 5 lists, <code>previousPositions</code>,  <code>previousOrientations</code>,
 * <code>previousVisibleObjects</code>, <code>previousPositionNames</code>, and <code>previousRoomNames</code>.
 * It happens when the user has left the regions to the next non-move atom (i.e. usually a type of maniupulate-action).
 * This is done in the <code>syncPlanToPlayer()</code> method,
 * which gets called in the <code>handleStatusInfo()</code> method of the NlgServer.
 * The central method is <code>syncPlanToPlayer</code>,
 * which gets called through the <code>handleStatusInformation</code> of the NlgSystem class.
 * The nested class <code>News</code> contains binary truth variables which indicate the difference
 * to the previous call of <code>syncPlanToPlayer</code>
 * @author Johannes
 *
 */
public class BigBrother {

    /**Necessary for the class InstructionTarget.
     * Defines the type of target the player has to reach in order for the task to be fulfilled.
     * It's an internal helper type just used to determine the value of news.playerHasCompletedTask
     * @author Johannes
     *
     */
    public enum CallingMethod {

        HANDLEACTION,
        HANDLESTATUSINFORMATION,
        UNDEFINED
    }

    /** This nested class keeps changes from the previous call of <code>syncPlanToPlayer</code>
     * @author Johannes
     *
     */
    public class News implements Cloneable {

        boolean createdNewPlan;
        boolean lostHighLevelTrack;
        boolean endOfHighLevelTrack;
        boolean playerHasMoved;
        Double playerHasMovedBy;
        boolean playerHasTurned;
        boolean wasOnTrack = true;
        Double playerHasTurnedBy;
        boolean playerHasStopped;
        boolean playerHasCompletedTask;
        CallingMethod callingMethod = CallingMethod.UNDEFINED;

        public Object clone() throws CloneNotSupportedException {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                System.out.println("Cloning not supported in class BigBrother.News: " + e.getMessage());
                return new Object();
            }
        }
    }

    /**Necessary for the class InstructionTarget.
     * Defines the type of target the player has to reach in order for the task to be fulfilled.
     * It's an internal helper type just used to determine the value of news.playerHasCompletedTask
     * @author Johannes
     *
     */
    public enum InstructionTargetType {

        REGION,
        ANGLE,
        ACTION,
        UNDEFINED,
        INITIALIZED
    }

    /** This nested class keeps changes from the previous call of <code>syncPlanToPlayer</code>
     * @author Johannes
     *
     */
    public class InstructionContainer {

        InstructionTargetType targetType;
        String targetRegion;
        String targetObject;
        Double targetAngle;
        String instructionPhrase;
        Position launchedPosition;
        Orientation launchedOrientation;

        InstructionContainer() {
            targetType = InstructionTargetType.UNDEFINED;

        }
    }
    // Core components:
    private SaarNlgSystem nlg; // Offers all necessary objects: World, Discretizer, BigBrother, InstructionGenerator, AlerGenerator
    public List<Atom> currentPlan;
//    private List<Alert> globalAlerts;
    // We want to keep track of what the user has been doing:
    private Integer previousElementsSize;
    public List<Position> previousPositions;
    public List<Orientation> previousOrientations;
    public List<List<String>> previousVisibleObjects;
    public List<String> previousPositionNames;
    public List<Room> previousRooms;
    public List<Instruction> previousInstructions;
    private List<BigBrother.News> previousNews;
    // Used for hasPerformedExpectedAction. Only saves the state of the World before the last action.
    public World previousWorld;
    // To track changes since the last call of handleStatusInformation():
    public News news;
    
    long lastSentMessageTimeStamp;
    long lastSentMessageReadingDuration;
    
    public InstructionContainer instructionTarget;
    // A small world which
//    private World smallWorld;
    // History of the already visited rooms
    private History history;

    private String targetRegion;
    private double previousDistanceToTargetRegion;

    /**
     * @author Boris Fersing
     * @return the history (visited rooms, seen objects)
     */
    public History getHistory() {
        return history;
    }

    public void updateHistoryObjects(List<String> visibleObjects) {
        history.updateObjects(visibleObjects);
    }

    /** Constructor, takes a SaarNlgSystem variable which we need
     * to access the getDiscretizer() and getWorld() methods.
     * @param nlgSystem	We need a SaarNlgSystem to access the getDiscretizer() and getWorld() methods.
     * @throws NlgServerException
     */
    public BigBrother(SaarNlgSystem nlgSystem) throws NlgServerException {
        this.nlg = nlgSystem;
        this.news = new News();
        this.instructionTarget = new InstructionContainer();
        this.history = new History();
        
        this.previousPositions = new ArrayList<Position>();
        this.previousOrientations = new ArrayList<Orientation>();
        this.previousVisibleObjects = new ArrayList<List<String>>();
        this.previousPositionNames = new ArrayList<String>();
        this.previousRooms = new ArrayList<Room>();
        this.previousNews = new ArrayList<BigBrother.News>();
        this.previousInstructions = new ArrayList<Instruction>();
        this.previousElementsSize = 0;

        lastSentMessageTimeStamp = 0L;
        lastSentMessageReadingDuration = 0L;
        
        // Get the initial plan:
        try {
            currentPlan = nlg.getPlan(nlg.getWorld().getGoals());
            news.createdNewPlan = true;
        } catch (IOException e) {
            throw new NlgServerException(e);
        }

        this.previousWorld = (World) this.nlg.getWorld().clone(); // We assign it a new value after each action performed.
    }

    /** This method uses the <code>cleanPlan()</code> method to remove done atoms.
     * It then decides if a new plan needs to get created
     * (i.e. if the user left the regions to the next non-move action).
     * This method also updates the variables in the nested News class,
     * which keeps information about what has changed since the last <code>handleStatusInformation</code> call
     * (in the <code>NlgSystem</code> class).
     * Must get called only once per "cycle"
     * (one cycle is to understand as a call of <code>handleStatusInformation</code>)
     * @param position The current position of the player in the GIVE world.
     * @param nextActionAtomIndex
     * @throws NlgServerException
     */
    public void syncPlanToPlayer(Position position, Orientation orientation, List<String> visibleObjects, CallingMethod callingMethod)
            throws NlgServerException {
        String currentPositionName = this.nlg.getDiscretizer().getPositionName(position);

        // Update the history
        history.update(this.nlg.getWorld().getRoomAtPosition(position.getX(), position.getZ()).getName(), visibleObjects);

        // Update field "News" (must get called before adding the current position etc. data to the previous variables)
        generateNews(position, orientation, visibleObjects, callingMethod);

        // In this section we log positions, orientations, visible objects and region names and room names and the news file.
        previousPositions.add(position);
        previousOrientations.add(orientation);
        previousVisibleObjects.add(visibleObjects); 
        previousPositionNames.add(currentPositionName);
        // RegionDiscretizer rd = new RegionDiscretizer(nlg.getWorld());
        // previousRooms.add(this.nlg.getDiscretizer().getRegionInformation(previousPositionNames.get(previousPositionNames.size()-1)).getRoom());
        previousRooms.add(this.nlg.getWorld().getRoomAtPosition(position.getX(), position.getZ()));
        try {
            previousNews.add((BigBrother.News) this.news.clone());
        } catch (Exception e) {
            System.out.println("Cloning not supported: " + e.getMessage());
        }
        previousElementsSize++;
        if (GIVEUtils.debug()) {
            System.out.println("syncPlanToPlayer(): Current data: Position = " + previousPositions.get(previousElementsSize - 1) + " Position name = " + previousPositionNames.get(previousElementsSize - 1) + " Room Name = " + previousRooms.get(previousElementsSize - 1).getName() + " Orientation = " + previousOrientations.get(previousElementsSize - 1) + " ");
        }

        // Do we have a "currentPlan"?
        if (((ArrayList<Atom>) currentPlan).isEmpty()) {
            // Sth. went wrong after performing connectionEstablished()
            throw new NlgServerException("No plan.");
        }

        // We clean the plan (remove unnecessary items) and get the updated index of the next Action:
        int nextActionAtomIndex = cleanPlan(position);

        List<String> targetRegionsToNextAction = new ArrayList<String>();

        // Put all regions into the targetRegionsToNextAction array
        for (int i = 0; i <= nextActionAtomIndex; i++) {
            Atom atomI = (Atom) currentPlan.get(i);
            targetRegionsToNextAction.add(getTargetRegionName(atomI));
            // If the atom is a "move" action, we use also the target region:
            String action = (atomI.getPredicate());
            if (action.equals("move")) {
                targetRegionsToNextAction.add(atomI.getArgumentAsString(0));
            }
        }

        
        if (getLastSentInstruction().isHighLevel()) {

            if (getLastSentInstruction().getPath().updatePath(currentPositionName)
                    && !getLastSentInstruction().getPath().targetReached()) {
                GIVEUtils.writeDebug("Player is on High level path : " + getLastSentInstruction().getPath().getCurrentPath());
                news.createdNewPlan = false;
                news.endOfHighLevelTrack = false;
                news.lostHighLevelTrack = false;
                return;
            } else if (getLastSentInstruction().getPath().updatePath(currentPositionName)
                    && getLastSentInstruction().getPath().targetReached()) {
                GIVEUtils.writeDebug("End of high level path : Target reached");
                news.createdNewPlan = false;
                news.endOfHighLevelTrack = true;
                news.lostHighLevelTrack = false;
            } else {
                GIVEUtils.writeDebug("Player no more on high level path");
                news.endOfHighLevelTrack = true;
                news.lostHighLevelTrack = true;
                news.createdNewPlan = true;
                return;
            }

        } else {

            // Is player on any of those?
            if (targetRegionsToNextAction.contains(currentPositionName) && isPlayerOnTrack(targetRegionsToNextAction, nlg.getWorld())) {
                // The player *is* on one of the tracks, so we don't create a new plan.
                //System.out.println("Player is on track. No new plan.");
                news.createdNewPlan = false;
                news.wasOnTrack = true;
                return; 
            } else {
                // The player lost the track --> Re-plan.
                if (!news.wasOnTrack || !isPlayerOnTrack(targetRegionsToNextAction, nlg.getWorld())) { 
                    try {
                        currentPlan = nlg.getPlan(nlg.getWorld().getGoals());
                        news.createdNewPlan = true;
                        return; // We generated a new plan
                    } catch (IOException e) {
                        System.out.println(e.getMessage());
                        throw new NlgServerException(e);
                    }
                }
            }
        }

    }

    /** Gets called by <code>syncPlanToPlayer</code>.
     * Analyzes the existing plan (the list of atoms, <code>currentPlan</code>, and removes the atoms (starting from atom index 0) whose regions the player has already been to.
     * @param position
     * @param nextActionAtomIndex
     * @return the index (starting from 0) of the next atom which is not equal to "move", from the collection of atoms of the plan.
     * @throws NlgServerException
     */
    private int cleanPlan(Position position) throws NlgServerException {
        // See if player was faster than the plan and
        // remove atoms from the plan where the player has already been to.
        // Instead of re-planning, which is more expensive.
        int nextActionAtomIndex = getNextActionAtomIndex();

        // determine target region and initialize pathfinding
        if( nextActionAtomIndex > 0 ) {
            Atom lastMoveAction = currentPlan.get(nextActionAtomIndex-1);
            assert lastMoveAction.getPredicate().equals("move");
            targetRegion = lastMoveAction.getArgumentAsString(1);
            previousDistanceToTargetRegion = Double.MAX_VALUE;
        } else {
            previousDistanceToTargetRegion = 0;
            targetRegion = nlg.getDiscretizer().getPositionName(nlg.getWorld().getPlayerPosition());
        }



        // System.out.println("\ncleanPlan: nextActionAtomIndex before cleaning = " + Integer.toString(nextActionAtomIndex));

        for (int i = nextActionAtomIndex - 1; i >= 0; i--) {
            // We start at the next non-move atom and go back to the start
            // We have ensured that values lower than nextActionAtomIndex are move atoms.
            // If current position equals the target region of the examined move-atom: remove
            String targetRegion = getTargetRegionName((Atom) currentPlan.get(i));
            String currentPosition = nlg.getDiscretizer().getPositionName(position);
            if (targetRegion.equals(currentPosition)) {
                // If yes, remove this and all previous items back to the start:
                for (int j = i; j >= 0; j--) {
                    // System.out.println("Removing " + ((Atom)currentPlan.get(i)).toString());
                    currentPlan.remove(j);
                    nextActionAtomIndex--;
                }
                break; // We are done with removing past atoms, time to leave.
            }
        }
        return nextActionAtomIndex;
    }

    /** Returns the index (lowest value: 0) of the next action atom (= the one not equal to "move") from the current plan.
     * @return the the index (lowest value: 0) of the next atom not equal to "move" from the current plan.
     */
    public int getNextActionAtomIndex() {
        // Now we want to find the next action atom (== "non-move-atom")
        // and put the index into nextActionAtomIndex
        int nextActionAtomIndex = -1;
        for (int i = 0; i < currentPlan.size(); i++) {
            if (!currentPlan.get(i).getPredicate().equals("move")) {
                // We store the next non-move index. It stays -1 in case no action is found.
                nextActionAtomIndex = i;
                break;
            }
        }
        return nextActionAtomIndex;
    }

    /** Returns the next atom of the plan which is not a move action.
     * @return an atom.
     */
    public Atom getNextActionAtom() {
        return currentPlan.get(getNextActionAtomIndex());
    }

    /** Returns a list of all (and only) action atoms of the plan which is not a move action.
     * @return an array of action atoms.
     */
    public ArrayList<Atom> getNextActionAtoms() {
        List<Atom> actionAtoms = new ArrayList<Atom>();

        for (Atom possibleActionAtom : currentPlan) {
            if (!possibleActionAtom.getPredicate().equals("move")) {
                actionAtoms.add(possibleActionAtom);
            }
        }
        return (ArrayList<Atom>) actionAtoms;
    }

    /*public boolean hasPerformedCorrectAction(){
    // If player is near to next action atom,
    // check if performed and expected parameters match.
    return true;
    }*/
    /** Returns the name of the target region of an atom of the plan, or the region of action (in case the atom contains not more than one region).
     * It can also be applied to the atom passed by <code>handleAction</code>.
     * @param atom
     * @return the name of the target region of an atom of the plan, or the region of action (in case the atom contains not more than one region).
     * @throws NlgServerException
     */
    public String getTargetRegionName(Atom atom) throws NlgServerException {
        String predicate = atom.getPredicate();

        // System.out.println("getTargetRegionName(): " + atom.toString());

        // getDiscretizer().getPositionName((Position)((List<Term>)actionInstance.getArguments()).get(3));

        Integer parameterIndexOfRegion = 0;
        if (predicate.equals("move")) {
            parameterIndexOfRegion = 1;
        } else if (predicate.equals("manipulate")) {
            parameterIndexOfRegion = 3;
        } else if (predicate.equals("manipulate-stateless")) {
            parameterIndexOfRegion = 4;
        } else if (predicate.equals("take-t1")) {
            parameterIndexOfRegion = 0;
        } else {
            throw (new NlgServerException("Invalid predicate in atom found: " + predicate));
        }

        if (((List<Term>) atom.getArguments()).get(parameterIndexOfRegion) instanceof Position) {
            // System.out.println("Type: Position");
            // System.out.println("Returning " + nlg.getDiscretizer().getPositionName((Position)((List<Term>)atom.getArguments()).get(parameterIndexOfRegion)));
            return nlg.getDiscretizer().getPositionName((Position) ((List<Term>) atom.getArguments()).get(parameterIndexOfRegion));
        } else {
            // System.out.println("Type: String");
            // System.out.println("Returning " + atom.getArgumentAsString(parameterIndexOfRegion));
            return atom.getArgumentAsString(parameterIndexOfRegion);
        }

    }

    /** Returns a list of regions of an atom.
     * @param atom
     * @return a list of regions of an atom. 2 elements for the "move" atom, 1 for the other types of atoms.
     * @throws NlgServerException
     */
    public List<String> getAtomRegions(Atom atom) throws NlgServerException {
        String predicate = atom.getPredicate();

        List<String> atomRegions = new ArrayList<String>();

        if (predicate.equals("move")) {
            atomRegions.add(atom.getArgumentAsString(0));
            atomRegions.add(atom.getArgumentAsString(1));
        } else if (predicate.equals("manipulate")) {
            atomRegions.add(atom.getArgumentAsString(3));

        } else if (predicate.equals("manipulate-stateless")) {
            atomRegions.add(atom.getArgumentAsString(4));
        } else if (predicate.equals("take-t1")) {
            atomRegions.add(atom.getArgumentAsString(0));
        } else {
            throw (new NlgServerException("Invalid predicate in atom found: " + predicate));
        }
        return atomRegions;
    }

    /** Not working �> not used. Should have substituted <code>compareExpectedPerformed</code>
     * @param performedAtom
     * @param updates
     * @return <code>true</code> if the two atoms are equal, <code>false</code> if not.
     * @throws NlgServerException
     */
    @Deprecated
    public boolean hasPerformedExpectedAction(Atom performedAtom, List<Formula> updates) {
        System.out.println("hasPerfromedExpectedAction()");
        Atom expectedAtom = (Atom) give.formula.parser.Parser.parse(getNextActionAtom().toString());
        Action expectedAction = previousWorld.findAction(expectedAtom);
        List<Formula> expectedLiterals = expectedAction.apply(expectedAtom, previousWorld);
        if (GIVEUtils.debug()) {
            System.out.println("After-performed literals: " + give.util.StringTools.join(expectedLiterals, ", "));
            System.out.println("Comparing atoms: " + performedAtom.toString() + " and " + expectedAtom.toString());
            System.out.println("Performed world: " + this.nlg.getWorld().toString());
            System.out.println("Expected world: " + previousWorld.toString());
            System.out.println("After-expected literals: " + give.util.StringTools.join(updates, ", "));
        }
        if (expectedLiterals.equals(updates)) {
            return true;
        } else {
            return false;
        }
    }

    /** Compares two atoms on equality (typically a performed action passed by the <code>handleAction</code> method and the expected atom (= first atom of the <code>currentPlan</code>).
     * Does NOT take into account the region parameter.
     * @param expectedAtom
     * @param performedAtom
     * @return <code>true</code> if the two atoms are equal, <code>false</code> if they are not.
     * @throws NlgServerException
     */
    public boolean compareExpectedPerformed(Atom expected, Atom performed) throws NlgServerException {
        /* We need a SLIGHTLY more complicated method than return expectedAtom.toString().equals(performedAtom.toString());
         * to compare the atoms, because the the position part of the atoms we get from handleAction
         * has the form of COORDINATES, whereas the position part of the plan's atom has a region NAME
         */

        Atom expectedAtom = expected;
        Atom performedAtom = performed;

        if (expectedAtom.getPredicate().equals("move")) {
            expectedAtom = getNextActionAtom();
        }

        String predicateExpected = expectedAtom.getPredicate();
        String predicatePerformed = performedAtom.getPredicate();

        if (!predicateExpected.equals(predicatePerformed)) {
            return false; // The predicates don't match, so no need to go ahead
        } // "Else" can only occur for non-move atoms (move doesn't trigger handleAction(),
        // and if the user performed a move, this method will return false (above)
        else if (predicatePerformed.equals("manipulate")) { // manipulate has 4 parameters, the 4th one of which needs to converted from coordinates to a region name. We don't compare the regions, though. Would be: !expectedAtom.getArgumentAsString(3).equals(nlg.getDiscretizer().getPositionName(performedAtom.getArgumentAsString(3))
            if (!expectedAtom.getArgumentAsString(0).equals(performedAtom.getArgumentAsString(0))
                    || !expectedAtom.getArgumentAsString(1).equals(performedAtom.getArgumentAsString(1))
                    || !expectedAtom.getArgumentAsString(2).equals(performedAtom.getArgumentAsString(2))) {
                return false;
            } else {
                return true;
            }

        } else if (predicatePerformed.equals("manipulate-stateless")) { // manipulate-stateless has 5 parameters, the 5th one of which needs to converted from coordinates to a region name
            if (!expectedAtom.getArgumentAsString(0).equals(performedAtom.getArgumentAsString(0))
                    || !expectedAtom.getArgumentAsString(1).equals(performedAtom.getArgumentAsString(1))
                    || !expectedAtom.getArgumentAsString(2).equals(performedAtom.getArgumentAsString(2))
                    || !expectedAtom.getArgumentAsString(3).equals(performedAtom.getArgumentAsString(3))) {
                return false;
            } else {
                return true;
            }

        } else if (predicatePerformed.startsWith("manipulate-stateless-") && !predicatePerformed.equals("manipulate-stateless")) {
            return true;
        } else if (predicatePerformed.equals("take-t1")) { // take-t1 has 1 parameter, which needs to converted from coordinates to a region name
            return true;
        } else {
            if (GIVEUtils.debug()) {
                System.out.println("Found unhandled predicted action: " + predicatePerformed);
            }
            throw (new NlgServerException("Found an unknown or unexpected(move?) performed action: " + predicatePerformed));
        }
    }

    /** Returns the position of the previous <code>handleStatusInformation</code> call.
     * @return the position of the previous <code>handleStatusInformation</code> call.
     */
    public Position getLastPosition() {
        return previousPositions.get(previousPositions.size() - 1);
    }

    /** Returns the orientation of the previous <code>handleStatusInformation</code> call.
     * @return the orientation of the previous <code>handleStatusInformation</code> call.
     */
    public Orientation getLastOrientation() {
        return previousOrientations.get(previousOrientations.size() - 1);
    }

    /** Returns the visible objects of the previous <code>handleStatusInformation</code> call.
     * @return the visible objects of the previous <code>handleStatusInformation</code> call.
     */
    public List<String> getLastVisibleObjects() {
        return previousVisibleObjects.get(previousVisibleObjects.size() - 1);
    }

    /** Returns the position name of the previous <code>handleStatusInformation</code> call.
     * @return the position name of the previous <code>handleStatusInformation</code> call.
     */
    public String getLastPositionName() {
        return previousPositionNames.get(previousPositionNames.size() - 1);
    }

    /** Returns the Instruction of the previous <code>handleStatusInformation</code> call.
     * @return the Instruction of the previous <code>handleStatusInformation</code> call.
     */
    public Instruction getLastInstruction() {
        if (previousInstructions.size() > 0) {
            return previousInstructions.get(previousInstructions.size() - 1);
        } else {
            return new Instruction("");
        }
    }

    /** Returns the Instruction of the previous <code>handleStatusInformation</code> call.
     * @return the Instruction of the previous <code>handleStatusInformation</code> call.
     */
    public Instruction getLastSentInstruction() {
        for (int i = previousInstructions.size() - 1; i >= 0; i--) {
            if (previousInstructions.get(i).hasBeenSent) {
                return previousInstructions.get(i);
            }
        }
        return new Instruction("");
    }

    /** Returns the room object of the previous <code>handleStatusInformation</code> call.
     * @return the room object of the previous <code>handleStatusInformation</code> call.
     */
    public Room getLastRoom() {
        return previousRooms.get(previousRooms.size() - 1);
    }

    /** Adds the argument "Instruction" to the list of previous Instruction objects.
     * @param instruction
     */
    public void archiveInstruction(Instruction instruction) {
        previousInstructions.add(instruction);
    }

    /** Gets called only in the beginning of <code>synPlanToPlayer</code>.
     * Saves the information on differences to the previous call of <code>handleStatusInformation</code> in the nested news object.
     * @param position
     * @param orientation
     * @param visibleObjects
     * @throws NlgServerException
     */
    private void generateNews(Position position, Orientation orientation, List<String> visibleObjects, CallingMethod callingMethod) throws NlgServerException {
        if (GIVEUtils.debug()) {
            System.out.println("generateNews()");
        }

        this.news.callingMethod = callingMethod;

        // createdNewPlan gets updated whenever the getPlan() method gets called.
        // We set it to false in the beginning:
        this.news.createdNewPlan = false;

        if (this.previousElementsSize == 0) {
            return;
        }

        // Has the player moved?
        if (getLastPosition().equals(position)) {
            this.news.playerHasMoved = false;
            this.news.playerHasMovedBy = .0;
            if (GIVEUtils.debug()) {
                System.out.println("Player has same position.");
            }
        } else {
            this.news.playerHasMoved = true;
            this.news.playerHasMovedBy = Math.sqrt(Math.pow(position.getX() - getLastPosition().getX(), 2) + Math.pow(position.getZ() - getLastPosition().getZ(), 2));
            if (GIVEUtils.debug()) {
                System.out.println("Player has new position (moved by " + this.news.playerHasMovedBy.toString() + ")");
            }
        }

        // Has the player turned? (Simple version: we take the value from 1.2 seconds ago.)
        // "> 1" because we need at least one past entry
        if (previousElementsSize > 1) {
            try {
                Integer stepsBack = 4;
                if (previousOrientations.get((previousElementsSize < stepsBack ? 0 : previousElementsSize - stepsBack)).equals(orientation)) {
                    this.news.playerHasTurned = false;
                    this.news.playerHasTurnedBy = .0;
                    if (GIVEUtils.debug()) {
                        System.out.println("Player has same orientation.");
                    }
                } else {
                    this.news.playerHasTurned = true;
                    this.news.playerHasTurnedBy = GeometryTools.getAngle(previousOrientations.get((previousElementsSize < stepsBack ? 0 : previousElementsSize - stepsBack)), orientation);
                    if (GIVEUtils.debug()) {
                        System.out.println("Player has new orientation (turned by " + this.news.playerHasTurnedBy.toString() + ")");
                    }
                }
            } catch (Exception e) {
                System.out.println("Error in calculation of orientation: " + e.getMessage());
            }
        }


        /* Has the player stopped?
         * We declare the player has stopped when they haven't moved for 7 cycles (� 200 ms) [first block in the try block]
         * or has not moved for 30 cycles [second block in the try block]
         *
         * */

        if (this.previousElementsSize == 1) {
            this.news.playerHasStopped = true; // in the beginning, the player is considered to have stopped.
        } else {
            this.news.playerHasStopped = false; // We assume they have not stopped. That prevents us from triggering instruction generation too often.
        }
        try {
            Integer stepsStopsBack = 7;
            if (previousPositions.size() > stepsStopsBack) {
                // We check all but the oldest entry in the loop:
                for (int i = previousPositions.size() - 1; i > (previousPositions.size() - stepsStopsBack); i--) {
                    if (position.equals(previousPositions.get(i))) {
                        this.news.playerHasStopped = true;
                    } else {
                        this.news.playerHasStopped = false;
                        break;
                    }
                }
                // If playerHasStopped is true (i.e. it's a candidate), the oldest entry must be not be equal to position (to only set the value once per stop):
                if (this.news.playerHasStopped
                        && position.equals(previousPositions.get(previousPositions.size() - stepsStopsBack))) {
                    // It IS equal, so it seems as we have set the value already before.
                    this.news.playerHasStopped = false;
                }
            }

            // We also want to trigger "stopped" when the player has not moved for a long time:
            Integer stepsNewsBack = 30;
            if (previousNews.size() > stepsNewsBack
                    && previousNews.get(previousNews.size() - stepsNewsBack).playerHasStopped == true
                    && previousPositions.get(previousNews.size() - stepsNewsBack).equals(position)) {
                this.news.playerHasStopped = true;
            }
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
            e.printStackTrace();
        }
        GIVEUtils.writeDebug("The player has " + (this.news.playerHasStopped ? "" : "not ") + "stopped. previousPositions.size() is " + Integer.toString(previousPositions.size()));

        /* Has the player reached the target of the task?
         * What we basically want to do is:
         * Avoid to generate a new instruction when reaching the target region,
         * if the old instruction (generated in a previous region) is still valid
         * AND referring to an action (and not to a movement) */
        switch (this.instructionTarget.targetType) {
            case ACTION:
                /* It is handleAction() which modifies the currentPlan,
                 * so there's no way here (as implemented) to determine if the task has been completed
                 * (by doing the correct action)
                 */
                this.news.playerHasCompletedTask = false; // it won't ever be true if the targetType is ACTION, because we are dealing with it in the nlgSystem's handleAction()
                break;
            case REGION:
                if (instructionTarget.targetRegion.equals(nlg.getDiscretizer().getPositionName(position))
                        && currentPlan.get(0).getPredicate().equals("move")) {
                    this.news.playerHasCompletedTask = true;
                } else {
                    this.news.playerHasCompletedTask = false;
                }
                break;
            case ANGLE:
                if (GIVEUtils.debug()) {
                    System.out.println("Angle to target (" + this.instructionTarget.targetRegion + "): " + Double.toString(position.getOrientationTo(nlg.getDiscretizer().getApproximatePosition(this.instructionTarget.targetRegion)).getAngleTo(orientation)));
                }
                if (Math.abs(position.getOrientationTo(nlg.getWorld().getPosition(this.instructionTarget.targetObject)).getAngleTo(orientation)) < .2
                        || visibleObjects.contains(this.instructionTarget.targetObject)) {
                    this.news.playerHasCompletedTask = true;
                    if (GIVEUtils.debug()) {
                        System.out.println("visible: " + visibleObjects.contains(this.instructionTarget.targetRegion));
                        System.out.println("angle below .75: " + (position.getOrientationTo(nlg.getDiscretizer().getApproximatePosition(this.instructionTarget.targetRegion)).getAngleTo(orientation) < .75));
                    }
                } else {
                    this.news.playerHasCompletedTask = false;
                }
                break;
            default:
                this.news.playerHasCompletedTask = false;
                if (GIVEUtils.debug()) {
                    System.out.println("instructionTargetType = " + this.instructionTarget.targetType.toString() + ". This should not have happened.");
                }
                break;
        }
    }

    /** Tells if the instruction text should include an "again".
     * It is only of use for the Instruction Generator class.
     * @param instruction
     * @return <code>true</code> if the same instruction text has been sent to the user before and the target atom is not the same as before.
     */
    public boolean phraseShouldContainAgain(Instruction instruction) throws NullPointerException {
        /* If text is the same as the previously sent we still have to verify
         * that the target is a new one (and not the same, with a repeatedly sent instruction)
         */
        try {

            // If atom differs, but target object not.
            Instruction lastSentInstruction = getLastSentInstruction();
            if (!lastSentInstruction.targetAtom.equals(instruction.targetAtom)
                    && lastSentInstruction.targetObjectId.equals(instruction.targetObjectId)
                    && !news.createdNewPlan) {
                return true;
            } else {
                return false;
            }


//			if (instruction.targetAtom.equals(lastSentInstruction.targetAtom)) {
//				return false; // The same (not only equal) instruction is referred to, instruction should actually get discarded.
//			}
//			else {
//				if (instruction.instructionPhrase.equals(lastSentInstruction.instructionPhrase)
//						&& ! lastSentInstruction.isAgainInstruction) {
//					/* Covers:
//					 * Example: last instruction = "turn left" [isAgainInstruction==false], next candidate instruction = "turn left"
//					 * Other:   last instruction = "turn left again" [isAgainInstruction==true], next candidate instruction = "turn left"
//					 */
//					return true;
//				}
//				else{
//					return false;
//				}
//			}*/

        } catch (NullPointerException e) {
            return false;
        }
    }

    /** Tells if the instruction text should include an "again".
     * It is only of use for the Instruction Generator class.
     * @param targetAtom
     * @param targetObjectId
     * @return <code>true</code> if the same instruction text has been sent to the user before and the target atom is not the same as before.
     * @throws NullPointerException
     */
    public boolean phraseShouldContainAgain(Atom targetAtom, String targetObjectId) throws NullPointerException {
        /* If text is the same as the previously sent we still have to verify
         * that the target is a new one (and not the same, with a repeatedly sent instruction)
         */
        try {

            // If atom differs, but target object not.
            Instruction lastSentInstruction = getLastSentInstruction();
            if (lastSentInstruction.targetAtom != targetAtom
                    && lastSentInstruction.targetObjectId.equals(targetObjectId)
                    && !news.createdNewPlan) {
                if (lastSentInstruction.targetAtom != targetAtom) {
                    System.out.println("not same atom");
                }
                return true;
            } else {
                return false;
            }
        } catch (NullPointerException e) {
            return false;
        }
    }

    /** Tells if the same instruction (text) to the same target has been sent before. Can be used to suppress repeated sending of instructions. Should be used just before sending the instruction to the client.
     * @param instruction
     * @return <code>true</code> is the text and targetAtom are the same as the last one sent.
     */
    public boolean textAndTargetIsSameAsPrevious(Instruction instruction) throws NullPointerException {
        if (instruction.equals(null)) {
            return false;
        }

        /* If text is the same as the previously sent
         * and the targetAtom is also the same as the previous one, return true
         */
        try {
            Instruction lastSentInstruction = getLastSentInstruction();
            if (lastSentInstruction.instructionPhrase.equals(instruction.instructionPhrase)
                    && lastSentInstruction.targetAtom.equals(instruction.targetAtom)) {
                return true;
            } else {
                return false;
            }
        } catch (NullPointerException e) {
            return false;
        }
    }

    /** Tells if the same instruction (text) to the same target has been sent before, *probably*. Can be used to suppress repeated sending of instructions. Should be used just before sending the instruction to the client.
     * @param instruction
     * @return <code>true</code> is the text and targetAtom are the same as the last one sent.
     */
    public boolean textAndTargetIsPrettySameAsPrevious(Instruction instruction) throws NullPointerException {
        if (instruction.equals(null)) {
            GIVEUtils.writeDebug("instruction (as parameter) is null");
            return false;
        }

        /* If text is the same as the previously sent
         * and the targetAtom is also the same as the previous one, return true
         */
        try {
            Instruction lastSentInstruction = getLastSentInstruction();
            GIVEUtils.writeDebug("last sent instruction = " + lastSentInstruction.instructionPhrase);
            GIVEUtils.writeDebug("new instruction = " + instruction.instructionPhrase);
            if (lastSentInstruction.instructionPhrase.contains(instruction.instructionPhrase)
                    && instruction.targetAtom.equals(lastSentInstruction.targetAtom)) {
                return true;
            } else {
                return false;
            }
        } catch (NullPointerException e) {
            return false;
        }
    }

    /**
     * Checks if the player is really on the right track
     * @author Alexander
     */
    private boolean isPlayerOnTrack(List<String> regions, World w) {
        String currentRegion = nlg.getDiscretizer().getPositionName(nlg.getWorld().getPlayerPosition());
        double currentDistance = nlg.getDistanceBetweenRegions(currentRegion, targetRegion);

        if( currentDistance > previousDistanceToTargetRegion ) {
            return false;
        } else {
            previousDistanceToTargetRegion = currentDistance;
            return true;
        }

        /*
        Atom targetAtom = getNextActionAtom();
        String targetId;
        Position playerPos = w.getPlayerPosition();

        if (targetAtom.getPredicate().contains("take-")) {
            targetId = targetAtom.getPredicate().replace("take-", "");
        } else {
            targetId = targetAtom.getArgumentAsString(0);
        }

        Position targetPos = w.getPosition(targetId);

        if (GIVEUtils.checkVisibility(playerPos, targetPos, w)) {
            news.wasOnTrack = true;
            return true;
        }

//        RegionDiscretizer r = new RegionDiscretizer(w);
        RegionDiscretizer r = (RegionDiscretizer) nlg.getDiscretizer();

        for (String region : regions) {
            Position regionPos = GIVEUtils.getCenterOfRegion(r, region);

            if (GIVEUtils.checkVisibility(playerPos, regionPos, w)) {
                news.wasOnTrack = true;
                return true;
            }
        }
        news.wasOnTrack = false;
        return false;
         *
         */
    }
    
    public boolean shouldGenerateInstruction(){
        // if (sentMessageTimeStamps.isEmpty())
        if (lastSentMessageTimeStamp == 0)
            return true;
        long now = System.currentTimeMillis();
        if (now - lastSentMessageTimeStamp > 5000)
            return true;
        else 
            return false;
    }
    
    public void sentMessage(long readingDuration){
        lastSentMessageTimeStamp = System.currentTimeMillis();
        lastSentMessageReadingDuration = readingDuration;
    }
    
    public boolean maySendMessage(){
        if (lastSentMessageTimeStamp == 0)
            return true;
        
        long now = System.currentTimeMillis();
        if (now - lastSentMessageTimeStamp > lastSentMessageReadingDuration)
            return true;
        else
            return false;
    }
}
